use std::{cell::OnceCell, str::Chars};

use crate::errors::PlundererError;

// Lexer

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Token {
    Alias,
    AllCols,
    ColIdx,
    ColStr,
    Colon,
    Dummy,
    ExcludeColIdx,
    ExcludeColStr,
    FidIdx,
    FidStr,
    File,
    IidIdx,
    IidStr,
    NoFid,
    NoHeader,
    PoundSign,
    Semicolon,
    Str(String),
    Uint(usize),
}

pub(crate) struct Lexer<'a> {
    chars: Chars<'a>,
    current: Option<char>,
}

impl<'a> Lexer<'a> {
    const SEP_CHARS: &'static [char] = &[';', ':', '#', '\t'];

    /// Creates a new lexer instance.
    pub(super) fn new(input: &'a str) -> Self {
        let mut lexer = Lexer {
            chars: input.chars(),
            current: None,
        };
        lexer.advance(); // Initialise `self.current`
        lexer
    }

    fn advance(&mut self) {
        self.current = self.chars.next();
    }

    fn advance_by(&mut self, n: usize) {
        for _ in 0..n {
            self.advance();
        }
    }

    fn peek(&self, n: usize) -> String {
        let mut result = String::new();
        let mut chars_iter = self.chars.clone();

        for _ in 0..n {
            if let Some(ch) = chars_iter.next() {
                result.push(ch);
            } else {
                break;
            }
        }

        result
    }

    /// peek at the nth character (self.current is 0)
    fn peek_at(&self, n: usize) -> Option<char> {
        let mut chars_iter = self.chars.clone();
        for _ in 1..n {
            chars_iter.next();
        }
        chars_iter.next()
    }

    /// `n` is the total amount of chars in the output String.
    #[allow(dead_code)]
    fn peek_with_current(&self, n: usize) -> String {
        let mut result = String::new();

        // Add current char
        if let Some(current_char) = self.current {
            result.push(current_char);
        }

        // Add following chars
        let mut chars_iter = self.chars.clone();
        for _ in 1..n {
            if let Some(ch) = chars_iter.next() {
                result.push(ch);
            } else {
                break;
            }
        }

        result
    }

    fn is_sep_at(&self, n: usize) -> bool {
        let ch = self.peek_at(n);
        if let Some(ch) = ch
            && Self::SEP_CHARS.contains(&ch)
        {
            true
        } else {
            false
        }
    }

    fn parse_number(&mut self) -> usize {
        let mut num_str = String::new();
        while let Some(ch) = self.current {
            if ch.is_ascii_digit() {
                num_str.push(ch);
                self.advance();
            } else {
                break;
            }
        }
        num_str.parse::<usize>().unwrap()
    }

    #[allow(dead_code)]
    fn parse_string(&mut self) -> String {
        let mut s = String::new();
        while let Some(ch) = self.current {
            if ch != ';' && ch != '#' {
                // white space is allowed. it is part of file name.
                s.push(ch);
                self.advance();
            } else {
                break;
            }
        }
        s
    }

    /// Tokenizes the input string into a vector of tokens.
    pub(crate) fn tokenize(&mut self) -> Vec<Token> {
        let mut tokens: Vec<Token> = Vec::new();
        while let Some(ch) = self.current {
            match ch {
                ' ' | '\t' | '\n' => {
                    self.advance();
                }
                ';' => {
                    tokens.push(Token::Semicolon);
                    self.advance();
                }
                ':' => {
                    tokens.push(Token::Colon);
                    self.advance();
                }
                '#' if self.peek(5) == "alias" => {
                    tokens.push(Token::PoundSign);
                    tokens.push(Token::Alias);
                    self.advance_by(6);
                }
                '#' if self.peek(5) == "dummy" => {
                    tokens.push(Token::PoundSign);
                    tokens.push(Token::Dummy);
                    self.advance_by(6);
                }
                '#' if self.peek(9) == "no-header" => {
                    tokens.push(Token::PoundSign);
                    tokens.push(Token::NoHeader);
                    self.advance_by(10);
                }
                '#' => {
                    tokens.push(Token::PoundSign);
                    self.advance_by(1);
                }
                'a' if self.peek(7) == "ll-cols" && self.is_sep_at(8) => {
                    tokens.push(Token::AllCols);
                    self.advance_by(8);
                }
                'c' if self.peek(6) == "ol-idx" && self.is_sep_at(7) => {
                    tokens.push(Token::ColIdx);
                    self.advance_by(7);
                }
                'c' if self.peek(6) == "ol-str" && self.is_sep_at(7) => {
                    tokens.push(Token::ColStr);
                    self.advance_by(7);
                }
                'e' if self.peek(14) == "xclude-col-idx" && self.is_sep_at(15) => {
                    tokens.push(Token::ExcludeColIdx);
                    self.advance_by(15);
                }
                'e' if self.peek(14) == "xclude-col-str" && self.is_sep_at(15) => {
                    tokens.push(Token::ExcludeColStr);
                    self.advance_by(15);
                }
                'f' if self.peek(3) == "ile" && self.is_sep_at(4) => {
                    tokens.push(Token::File);
                    self.advance_by(4);
                }
                'f' if self.peek(6) == "id-idx" && self.is_sep_at(7) => {
                    tokens.push(Token::FidIdx);
                    self.advance_by(7);
                }
                'f' if self.peek(6) == "id-str" && self.is_sep_at(7) => {
                    tokens.push(Token::FidStr);
                    self.advance_by(7);
                }
                'i' if self.peek(6) == "id-idx" && self.is_sep_at(7) => {
                    tokens.push(Token::IidIdx);
                    self.advance_by(7);
                }
                'i' if self.peek(6) == "id-str" && self.is_sep_at(7) => {
                    tokens.push(Token::IidStr);
                    self.advance_by(7);
                }
                'n' if self.peek(5) == "o-fid" && self.is_sep_at(6) => {
                    tokens.push(Token::NoFid);
                    self.advance_by(6);
                }
                '"' => {
                    self.advance();
                    let mut s = String::new();
                    while let Some(ch) = self.current {
                        if ch != '"' {
                            s.push(ch);
                            self.advance();
                        } else {
                            self.advance();
                            break;
                        }
                    }
                    tokens.push(Token::Str(s));
                }
                _ => match self.current {
                    Some(ch) => {
                        if ch.is_ascii_digit() {
                            let num = self.parse_number();
                            tokens.push(Token::Uint(num));
                        } else {
                            let mut s = String::new();
                            while let Some(ch) = self.current {
                                if !Self::SEP_CHARS.contains(&ch) {
                                    s.push(ch);
                                    self.advance();
                                } else {
                                    break;
                                }
                            }
                            tokens.push(Token::Str(s));
                        }
                    }
                    None => break,
                },
            }
        }

        tokens
    }
}

// Parser

use super::file::{ColumnSpec, FidSpec, FileSpec, IidSpec, SelectCol, SelectionSpec};

#[derive(Debug, Clone, PartialEq)]
pub(super) struct Parser<'a> {
    tokens: &'a [Token],
    position: usize,
}

impl<'a> Parser<'a> {
    pub(super) fn new(tokens: &'a [Token]) -> Self {
        Self {
            tokens,
            position: 0,
        }
    }

    fn current(&self) -> Option<&Token> {
        self.tokens.get(self.position)
    }

    fn advance(&mut self) {
        self.position += 1;
    }

    fn expect(&self, expected: &Token) -> Result<(), PlundererError> {
        match self.current() {
            Some(token) if std::mem::discriminant(token) == std::mem::discriminant(expected) => {
                Ok(())
            }
            Some(found) => Err(PlundererError::SyntaxError {
                expected: vec![expected.clone()],
                found: found.clone(),
            }),
            None => Err(PlundererError::SyntaxEofError {
                expected: vec![expected.clone()],
            }),
        }
    }

    /// Returns the content if current token is a string. If the token is
    /// a uint, it is converted to a string.
    fn expect_string(&self) -> Result<String, PlundererError> {
        match self.current() {
            Some(Token::Str(value)) => Ok(value.clone()),
            Some(Token::Uint(value)) => Ok(value.to_string()),
            Some(token) => Err(PlundererError::SyntaxError {
                expected: vec![Token::Str("".to_string())],
                found: token.clone(),
            }),
            None => Err(PlundererError::SyntaxEofError {
                expected: vec![Token::Str("".to_string())],
            }),
        }
    }

    /// Returns the content if current token is a uint.
    fn expect_uint(&self) -> Result<usize, PlundererError> {
        match self.current() {
            Some(Token::Uint(value)) => Ok(*value),
            Some(token) => Err(PlundererError::SyntaxError {
                expected: vec![Token::Uint(0)],
                found: token.clone(),
            }),
            None => Err(PlundererError::SyntaxEofError {
                expected: vec![Token::Uint(0)],
            }),
        }
    }

    pub(super) fn parse_file_spec(&mut self) -> Result<FileSpec, PlundererError> {
        let has_header: bool;
        let fid_spec_cell: OnceCell<FidSpec> = OnceCell::new();
        let iid_spec_cell: OnceCell<IidSpec> = OnceCell::new();
        let mut selection_spec_option: Option<SelectionSpec> = None;
        let mut exclusions: Vec<ColumnSpec> = Vec::new();

        // file:<file_path>(#no-header);
        self.expect(&Token::File)?;
        self.advance();
        self.expect(&Token::Colon)?;
        self.advance();
        let file_path = self.expect_string()?;
        self.advance();
        match self.expect(&Token::PoundSign) {
            Ok(_) => {
                self.advance();
                self.expect(&Token::NoHeader)?;
                self.advance();
                has_header = false;
            }
            Err(_) => {
                has_header = true;
            }
        }
        self.expect(&Token::Semicolon)?;
        self.advance();

        while let Some(token) = self.current() {
            match token {
                // `file:<file_path>(#no-header) has been parsed
                Token::File => {
                    let dup_file_path = self.expect_string()?;
                    let dup_has_header: bool;
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    match self.expect(&Token::PoundSign) {
                        Ok(_) => {
                            self.advance();
                            self.expect(&Token::NoHeader)?;
                            self.advance();
                            dup_has_header = false;
                        }
                        Err(_) => {
                            self.advance();
                            dup_has_header = true;
                        }
                    }
                    self.expect(&Token::Semicolon)?;

                    return Err(PlundererError::DuplicateError {
                        existing: {
                            let mut s = format!("file:{file_path}");
                            if !has_header {
                                s.push_str("#no-header");
                            }
                            s
                        },
                        new: {
                            let mut s = format!("file:{dup_file_path}");
                            if !dup_has_header {
                                s.push_str("#no-header");
                            }
                            s
                        },
                    });
                }
                // fid-idx:<fid_idx>|fid-str:<fid_str>|no-fid;
                Token::FidIdx => {
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let fid_idx = self.expect_uint()?;
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();
                    fid_spec_cell
                        .set(FidSpec::Index(fid_idx))
                        .map_err(|fid_spec| PlundererError::DuplicateError {
                            existing: format!("{:?}", fid_spec),
                            new: format!("fid-idx:{}", fid_idx),
                        })?;
                }
                Token::FidStr => {
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let fid_str = self.expect_string()?;
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();
                    fid_spec_cell
                        .set(FidSpec::Str(fid_str.clone()))
                        .map_err(|fid_spec| PlundererError::DuplicateError {
                            existing: format!("{:?}", fid_spec),
                            new: format!("fid-str:{fid_str}"),
                        })?;
                }
                Token::NoFid => {
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();
                    fid_spec_cell.set(FidSpec::Missing).map_err(|fid_spec| {
                        PlundererError::DuplicateError {
                            existing: format!("{:?}", fid_spec),
                            new: format!("no-fid"),
                        }
                    })?;
                }
                // iid-idx:<iid_idx>|iid-str:<iid_str>
                Token::IidIdx => {
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let iid_idx = self.expect_uint()?;
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();
                    iid_spec_cell
                        .set(IidSpec::Index(iid_idx))
                        .map_err(|iid_spec| PlundererError::DuplicateError {
                            existing: format!("{:?}", iid_spec),
                            new: format!("iid-idx:{iid_idx}"),
                        })?;
                }
                Token::IidStr => {
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let iid_str = self.expect_string()?;
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();
                    iid_spec_cell
                        .set(IidSpec::Str(iid_str.clone()))
                        .map_err(|iid_spec| PlundererError::DuplicateError {
                            existing: format!("{iid_spec:?}"),
                            new: format!("iid-str:{iid_str}"),
                        })?;
                }
                // (col-idx:<col_idx>|col-str:<col_str>)(#dummy|#alias:<alias>)|all-cols
                Token::ColIdx => {
                    let to_dummy_cell: OnceCell<bool> = OnceCell::new();
                    let alias_cell: OnceCell<String> = OnceCell::new();
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let col_idx = self.expect_uint()?;
                    self.advance();

                    while self.expect(&Token::PoundSign).is_ok() {
                        self.advance();
                        match self.current().ok_or(PlundererError::SyntaxEofError {
                            expected: vec![Token::Dummy, Token::Alias],
                        })? {
                            Token::Dummy => {
                                self.advance();
                                to_dummy_cell.set(true).map_err(|to_dummy| {
                                    PlundererError::DuplicateError {
                                        existing: {
                                            let mut s = format!("col-idx:{col_idx}");
                                            if to_dummy {
                                                s.push_str("#dummy");
                                            }
                                            if let Some(alias) = alias_cell.get() {
                                                s.push_str(&format!("#alias:{alias}"));
                                            }
                                            s
                                        },
                                        new: {
                                            let mut s = format!("col-idx:{col_idx}#dummy");
                                            if to_dummy {
                                                s.push_str("#dummy");
                                            }
                                            if let Some(alias) = alias_cell.get() {
                                                s.push_str(&format!("#alias:{alias}"));
                                            }
                                            s
                                        },
                                    }
                                })?;
                            }
                            Token::Alias => {
                                self.advance();
                                self.expect(&Token::Colon)?;
                                self.advance();
                                let alias_str = self.expect_string()?;
                                self.advance();

                                alias_cell.set(alias_str.clone()).map_err(|alias| {
                                    let mut s = format!("col-idx:{col_idx}");
                                    if to_dummy_cell.get().is_some_and(|to_dummy| *to_dummy) {
                                        s.push_str("#dummy");
                                    }
                                    s.push_str(&format!("#alias:{alias}"));

                                    PlundererError::DuplicateError {
                                        existing: s.clone(),
                                        new: {
                                            s.push_str(&format!("#alias:{alias_str}"));
                                            s
                                        },
                                    }
                                })?;
                            }
                            _ => {
                                return Err(PlundererError::SyntaxError {
                                    expected: vec![Token::Alias, Token::Dummy],
                                    found: self.current().unwrap().clone(),
                                });
                            }
                        };
                    }
                    self.expect(&Token::Semicolon)?;
                    self.advance();

                    let current_col = SelectCol {
                        spec: ColumnSpec::Index(col_idx),
                        to_dummy: *to_dummy_cell.get_or_init(|| false),
                        alias: alias_cell.get().map(|alias| alias.to_string()),
                    };

                    match &mut selection_spec_option {
                        None => {
                            selection_spec_option = Some(SelectionSpec::Cols(vec![current_col]));
                        }
                        Some(selection_spec) => match selection_spec {
                            SelectionSpec::All { to_dummy } => {
                                return Err(PlundererError::DuplicateError {
                                    existing: {
                                        let mut s = "all-cols".to_string();
                                        if *to_dummy {
                                            s.push_str("#dummy");
                                        }
                                        s
                                    },
                                    new: {
                                        let mut s = format!("col-idx:{col_idx}");
                                        if *to_dummy_cell.get_or_init(|| false) {
                                            s.push_str("#dummy");
                                        };
                                        if let Some(alias_str) = alias_cell.get() {
                                            s.push_str(&format!("#alias:{alias_str}"));
                                        };
                                        s
                                    },
                                });
                            }
                            SelectionSpec::Cols(cols) => {
                                cols.push(current_col);
                            }
                        },
                    }
                }
                Token::ColStr => {
                    let to_dummy_cell: OnceCell<bool> = OnceCell::new();
                    let alias_cell: OnceCell<String> = OnceCell::new();

                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let col_str = self.expect_string()?;
                    self.advance();

                    while self.expect(&Token::PoundSign).is_ok() {
                        self.advance();
                        match self.current().ok_or(PlundererError::SyntaxEofError {
                            expected: vec![Token::Dummy, Token::Alias],
                        })? {
                            Token::Dummy => {
                                self.advance();
                                to_dummy_cell.set(true).map_err(|to_dummy| {
                                    let mut s = format!("col-str:{col_str}");
                                    if to_dummy {
                                        s.push_str("#dummy");
                                    }
                                    if let Some(alias) = alias_cell.get() {
                                        s.push_str(&format!("#alias:{alias}"));
                                    }
                                    PlundererError::DuplicateError {
                                        existing: s.clone(),
                                        new: {
                                            s.push_str("#dummy");
                                            s
                                        },
                                    }
                                })?;
                            }
                            Token::Alias => {
                                self.advance();
                                self.expect(&Token::Colon)?;
                                self.advance();
                                let alias_str = self.expect_string()?;
                                self.advance();
                                alias_cell.set(alias_str.clone()).map_err(|alias| {
                                    let mut s = format!("col-str:{col_str}");
                                    if *to_dummy_cell.get_or_init(|| false) {
                                        s.push_str("#dummy");
                                    }
                                    s.push_str(&format!("#alias:{alias}"));
                                    PlundererError::DuplicateError {
                                        existing: s.clone(),
                                        new: {
                                            s.push_str(&format!("#alias:{alias_str}"));
                                            s
                                        },
                                    }
                                })?;
                            }
                            _ => {
                                return Err(PlundererError::SyntaxError {
                                    expected: vec![Token::Alias, Token::Dummy],
                                    found: self.current().unwrap().clone(),
                                });
                            }
                        };
                    }
                    self.expect(&Token::Semicolon)?;
                    self.advance();

                    let current_col = SelectCol {
                        spec: ColumnSpec::Str(col_str.clone()),
                        to_dummy: *to_dummy_cell.get_or_init(|| false),
                        alias: alias_cell.get().map(|alias| alias.to_string()),
                    };

                    match &mut selection_spec_option {
                        None => {
                            selection_spec_option = Some(SelectionSpec::Cols(vec![current_col]));
                        }
                        Some(selection_spec) => match selection_spec {
                            SelectionSpec::All { to_dummy } => {
                                return Err(PlundererError::DuplicateError {
                                    existing: {
                                        let mut s = "all-cols".to_string();
                                        if *to_dummy {
                                            s.push_str("#dummy");
                                        }
                                        s
                                    },
                                    new: {
                                        let mut s = format!("col-str:{col_str}");
                                        if *to_dummy_cell.get_or_init(|| false) {
                                            s.push_str("#dummy");
                                        };
                                        if let Some(alias_str) = alias_cell.get() {
                                            s.push_str(&format!("#alias:{alias_str}"));
                                        };
                                        s
                                    },
                                });
                            }
                            SelectionSpec::Cols(cols) => {
                                cols.push(current_col);
                            }
                        },
                    }
                }
                Token::AllCols => {
                    let mut to_dummy = false;
                    self.advance();
                    if self.expect(&Token::PoundSign).is_ok() {
                        self.advance();
                        self.expect(&Token::Dummy)?;
                        to_dummy = true;
                        self.advance();
                    }
                    self.expect(&Token::Semicolon)?;
                    self.advance();

                    match selection_spec_option {
                        Some(selection_spec) => {
                            let mut s = String::new();
                            match selection_spec {
                                SelectionSpec::All { to_dummy } => {
                                    s = String::from("all-cols");
                                    if to_dummy {
                                        s.push_str("#dummy");
                                    }
                                }
                                SelectionSpec::Cols(selectcol_vec) => {
                                    for select_col in selectcol_vec {
                                        match select_col.spec {
                                            ColumnSpec::Index(idx) => {
                                                s.push_str(&format!("col-idx:{idx}"));
                                            }
                                            ColumnSpec::Str(str) => {
                                                s.push_str(&format!("col-str:{str}"));
                                            }
                                        }
                                        if select_col.to_dummy {
                                            s.push_str("#dummy");
                                        }
                                        if let Some(alias) = select_col.alias {
                                            s.push_str(&format!("#alias:{alias}"));
                                        }
                                        s.push_str(";");
                                    }
                                }
                            }
                            return Err(PlundererError::DuplicateError {
                                existing: s.clone(),
                                new: {
                                    s.push_str("all-cols;");
                                    s
                                },
                            });
                        }
                        None => selection_spec_option = Some(SelectionSpec::All { to_dummy }),
                    }
                }
                // exclude-col-idx:<exclude-col-idx>|exclude-col-str:<exclude-col-str>
                Token::ExcludeColIdx => {
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let idx = self.expect_uint()?;
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();

                    exclusions.push(ColumnSpec::Index(idx));
                }
                Token::ExcludeColStr => {
                    self.advance();
                    self.expect(&Token::Colon)?;
                    self.advance();
                    let s = self.expect_string()?;
                    self.advance();
                    self.expect(&Token::Semicolon)?;
                    self.advance();

                    exclusions.push(ColumnSpec::Str(s));
                }
                _ => {
                    println!("Error on position {}", self.position);
                    return Err(PlundererError::SyntaxError {
                        expected: vec![
                            Token::File,
                            Token::FidIdx,
                            Token::FidStr,
                            Token::NoFid,
                            Token::IidIdx,
                            Token::IidStr,
                            Token::ColIdx,
                            Token::ColStr,
                            Token::AllCols,
                            Token::ExcludeColIdx,
                            Token::ExcludeColStr,
                        ],
                        found: token.clone(),
                    });
                }
            }
        }

        return Ok(FileSpec {
            file_path: file_path,
            has_header: has_header,
            fid_spec: fid_spec_cell.get_or_init(|| FidSpec::Index(0)).clone(),
            iid_spec: iid_spec_cell
                .get_or_init(|| match fid_spec_cell.get().unwrap() {
                    FidSpec::Index(fid) => IidSpec::Index(fid + 1),
                    _ => IidSpec::Index(1),
                })
                .clone(),
            selections: match selection_spec_option {
                Some(selection_spec) => selection_spec,
                None => SelectionSpec::All { to_dummy: false },
            },
            exclusions: exclusions,
        });
    }
}

#[cfg(test)]
mod lexer_tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4)
    }

    #[test]
    fn str_literal() {
        let s = "file";
        let expect = vec![Token::Str(s.to_string())];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn uint_literal() {
        let s = "123";
        let expect = vec![Token::Uint(123)];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn separator() {
        {
            let s = "#";
            let expect = vec![Token::PoundSign];
            let res = Lexer::new(s).tokenize();
            assert_eq!(expect, res);
        }
        {
            let s = ";";
            let expect = vec![Token::Semicolon];
            let res = Lexer::new(s).tokenize();
            assert_eq!(expect, res);
        }
        {
            let s = " ";
            let expect: Vec<Token> = vec![];
            let res = Lexer::new(s).tokenize();
            assert_eq!(expect, res);
        }
        {
            let s = "\t";
            let expect: Vec<Token> = vec![];
            let res = Lexer::new(s).tokenize();
            assert_eq!(expect, res);
        }
        {
            let s = ":";
            let expect = vec![Token::Colon];
            let res = Lexer::new(s).tokenize();
            assert_eq!(expect, res);
        }
    }

    #[test]
    fn file() {
        let s = "file:foo.txt;";
        let expect = vec![
            Token::File,
            Token::Colon,
            Token::Str("foo.txt".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn file_without_header() {
        let s = "file:foo.txt#no-header;";
        let expect = vec![
            Token::File,
            Token::Colon,
            Token::Str("foo.txt".to_string()),
            Token::PoundSign,
            Token::NoHeader,
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn fid_index() {
        let s = "fid-idx:1563;";
        let expect = vec![
            Token::FidIdx,
            Token::Colon,
            Token::Uint(1563),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn fid_str() {
        let s = "fid-str:foo;";
        let expect = vec![
            Token::FidStr,
            Token::Colon,
            Token::Str("foo".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn no_fid() {
        let s = "no-fid;";
        let expect = vec![Token::NoFid, Token::Semicolon];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn iid_idx() {
        let s = "iid-idx:0;";
        let expect = vec![
            Token::IidIdx,
            Token::Colon,
            Token::Uint(0),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn iid_str() {
        let s = "iid-str:foo;";
        let expect = vec![
            Token::IidStr,
            Token::Colon,
            Token::Str("foo".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn col_idx() {
        let s = "col-idx:0;";
        let expect = vec![
            Token::ColIdx,
            Token::Colon,
            Token::Uint(0),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn col_str() {
        let s = "col-str:foo;";
        let expect = vec![
            Token::ColStr,
            Token::Colon,
            Token::Str("foo".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn all_cols() {
        let s = "all-cols;";
        let expect = vec![Token::AllCols, Token::Semicolon];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn col_multiple() {
        let s = "col-idx:1;col-idx:2;col-str:bar;col-idx:10;";
        let expect = vec![
            Token::ColIdx,
            Token::Colon,
            Token::Uint(1),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(2),
            Token::Semicolon,
            Token::ColStr,
            Token::Colon,
            Token::Str("bar".to_string()),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(10),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn exclude_col_idx() {
        let s = "exclude-col-idx:1;";
        let expect = vec![
            Token::ExcludeColIdx,
            Token::Colon,
            Token::Uint(1),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn exclude_col_str() {
        let s = "exclude-col-str:bar;";
        let expect = vec![
            Token::ExcludeColStr,
            Token::Colon,
            Token::Str("bar".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn exclude_multiple_cols() {
        let s = "exclude-col-idx:1;exclude-col-str:bar;";
        let expect = vec![
            Token::ExcludeColIdx,
            Token::Colon,
            Token::Uint(1),
            Token::Semicolon,
            Token::ExcludeColStr,
            Token::Colon,
            Token::Str("bar".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }

    #[test]
    fn whole_instance() {
        let s = "\
file:foo.txt#no-header;\
fid-idx:0;iid-str:IID;\
col-idx:10;col-idx:15#dummy;col-str:Overspeed#alias:overspeed#dummy;\
exclude-col-idx:152;exclude-col-str:Gender;";
        let expect = vec![
            Token::File,
            Token::Colon,
            Token::Str("foo.txt".to_string()),
            Token::PoundSign,
            Token::NoHeader,
            Token::Semicolon,
            Token::FidIdx,
            Token::Colon,
            Token::Uint(0),
            Token::Semicolon,
            Token::IidStr,
            Token::Colon,
            Token::Str("IID".to_string()),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(10),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(15),
            Token::PoundSign,
            Token::Dummy,
            Token::Semicolon,
            Token::ColStr,
            Token::Colon,
            Token::Str("Overspeed".to_string()),
            Token::PoundSign,
            Token::Alias,
            Token::Colon,
            Token::Str("overspeed".to_string()),
            Token::PoundSign,
            Token::Dummy,
            Token::Semicolon,
            Token::ExcludeColIdx,
            Token::Colon,
            Token::Uint(152),
            Token::Semicolon,
            Token::ExcludeColStr,
            Token::Colon,
            Token::Str("Gender".to_string()),
            Token::Semicolon,
        ];
        let res = Lexer::new(s).tokenize();

        assert_eq!(expect, res);
    }
}

#[cfg(test)]
mod parser_tests {
    use super::*;

    #[test]
    fn test_simple() {
        let tokens = vec![
            Token::File,
            Token::Colon,
            Token::Str("path/to/foo.txt".to_string()),
            Token::Semicolon,
            Token::FidIdx,
            Token::Colon,
            Token::Uint(0),
            Token::Semicolon,
            Token::IidIdx,
            Token::Colon,
            Token::Uint(1),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(10),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(15),
            Token::Semicolon,
            Token::ColStr,
            Token::Colon,
            Token::Str("heart_rate".to_string()),
            Token::Semicolon,
            Token::ExcludeColIdx,
            Token::Colon,
            Token::Uint(11),
            Token::Semicolon,
            Token::ExcludeColStr,
            Token::Colon,
            Token::Str("Sex".to_string()),
            Token::Semicolon,
        ];

        let expected_file_spec = FileSpec {
            file_path: "path/to/foo.txt".to_string(),
            has_header: true,
            fid_spec: FidSpec::Index(0),
            iid_spec: IidSpec::Index(1),
            selections: SelectionSpec::Cols(vec![
                SelectCol {
                    spec: ColumnSpec::Index(10),
                    alias: None,
                    to_dummy: false,
                },
                SelectCol {
                    spec: ColumnSpec::Index(15),
                    alias: None,
                    to_dummy: false,
                },
                SelectCol {
                    spec: ColumnSpec::Str("heart_rate".to_string()),
                    alias: None,
                    to_dummy: false,
                },
            ]),
            exclusions: vec![ColumnSpec::Index(11), ColumnSpec::Str("Sex".to_string())],
        };

        let res = Parser::new(&tokens).parse_file_spec().unwrap();
        assert_eq!(expected_file_spec, res);
    }

    #[test]
    fn skip_as_can() {
        let tokens = vec![
            Token::File,
            Token::Colon,
            Token::Str("path/to/foo.txt".to_string()),
            Token::Semicolon,
        ];

        let expected_file_spec = FileSpec {
            file_path: "path/to/foo.txt".to_string(),
            has_header: true,
            fid_spec: FidSpec::Index(0),
            iid_spec: IidSpec::Index(1),
            selections: SelectionSpec::All { to_dummy: false },
            exclusions: Vec::new(),
        };

        let res = Parser::new(&tokens).parse_file_spec().unwrap();
        assert_eq!(expected_file_spec, res);
    }

    #[test]
    fn skip_fid_spec() {
        let tokens = vec![
            Token::File,
            Token::Colon,
            Token::Str("path/to/foo.txt".to_string()),
            Token::Semicolon,
            Token::IidIdx,
            Token::Colon,
            Token::Uint(5),
            Token::Semicolon,
        ];

        let expected_file_spec = FileSpec {
            file_path: "path/to/foo.txt".to_string(),
            has_header: true,
            fid_spec: FidSpec::Index(0),
            iid_spec: IidSpec::Index(5),
            selections: SelectionSpec::All { to_dummy: false },
            exclusions: Vec::new(),
        };

        let res = Parser::new(&tokens).parse_file_spec().unwrap();
        assert_eq!(expected_file_spec, res);
    }

    #[test]
    fn comprehensive_tokens() {
        let tokens = vec![
            Token::File,
            Token::Colon,
            Token::Str("path/to/foo.txt".to_string()),
            Token::PoundSign,
            Token::NoHeader,
            Token::Semicolon,
            Token::FidStr,
            Token::Colon,
            Token::Str("FID".to_string()),
            Token::Semicolon,
            Token::IidIdx,
            Token::Colon,
            Token::Uint(5),
            Token::Semicolon,
            Token::ColStr,
            Token::Colon,
            Token::Str("Hypertension".to_string()),
            Token::PoundSign,
            Token::Alias,
            Token::Colon,
            Token::Str("HT".to_string()),
            Token::Semicolon,
            Token::ColIdx,
            Token::Colon,
            Token::Uint(9),
            Token::PoundSign,
            Token::Alias,
            Token::Colon,
            Token::Str("ethnicity".to_string()),
            Token::PoundSign,
            Token::Dummy,
            Token::Semicolon,
            Token::ExcludeColIdx,
            Token::Colon,
            Token::Uint(99),
            Token::Semicolon,
            Token::ExcludeColStr,
            Token::Colon,
            Token::Str("Age".to_string()),
            Token::Semicolon,
        ];

        let expected_file_spec = FileSpec {
            file_path: "path/to/foo.txt".to_string(),
            has_header: false,
            fid_spec: FidSpec::Str("FID".to_string()),
            iid_spec: IidSpec::Index(5),
            selections: SelectionSpec::Cols(vec![
                SelectCol {
                    spec: ColumnSpec::Str("Hypertension".to_string()),
                    alias: Some("HT".to_string()),
                    to_dummy: false,
                },
                SelectCol {
                    spec: ColumnSpec::Index(9),
                    alias: Some("ethnicity".to_string()),
                    to_dummy: true,
                },
            ]),
            exclusions: vec![ColumnSpec::Index(99), ColumnSpec::Str("Age".to_string())],
        };

        let res = Parser::new(&tokens).parse_file_spec().unwrap();
        assert_eq!(expected_file_spec, res);
    }
}
